Skip to content

Conversation

erikhfj
Copy link
Member

@erikhfj erikhfj commented Oct 7, 2026

What this does

Spec work for #10. No runtime yet. This is the contract that the client and the server will both implement in M2.

What was done

schema.graphql

type User {
  id: ID!
  displayName: String!
}

type Review {
  id: ID!
  user: User!        # added
  rating: Int!
  comment: String!
  createdAt: DateTime!
  updatedAt: DateTime!
}

Reviews rendered without an author was a real gap. The brief asks that user-generated data is presented, and an unattributed review does not present it. displayName is deliberately not in UpsertReviewInput, so a review cannot be attributed to a name the sender did not earn.

AGENTS.md rule 5 was wrong. It said the UUID is "sent with every mutation". myReview, isInCrate and myCrate are queries and need the identity just as much. Rewritten to cover the header, the per-request context, the anonymous fallback, and why displayName is derived server-side.

docs/architecture.md decision 5. Even had the decision right ("sends it with each request") but not the transport. This adds:

  • why an X-User-Id header and not a GraphQL variable, a cookie, or Authorization
  • why it goes on every request
  • the missing / malformed / valid-but-new / valid behaviour table
  • why displayName is derived from the UUID rather than supplied

docs/data-model.md gains the user in the user-generated data section.

Why a header and not a GraphQL variable

myReview, isInCrate and myCrate currently take zero arguments, which is deliberate. A userId argument on each would mean threading $userId through every operation and would make identity look like data when it is really transport. The header is set once by an Apollo link and read once into the context.

It is also not Authorization, because nothing here is authenticated. Anyone can send any UUID. Calling it Authorization would be misleading when we are asked to defend this in a viva.

Missing or invalid

State myReview isInCrate myCrate Mutations
No header null false empty connection error: no client id
Malformed null false empty connection error: invalid client id
Valid, no row yet null false empty connection succeed, create the row
Valid, row exists the review as stored the crate succeed

Reads never fail because of identity state, so a bad header cannot take the search page down. Writes require it and say so rather than attributing the change to nobody.

What was tested

Validated the SDL with buildSchema:

SDL parses OK
Review fields: id, user, rating, comment, createdAt, updatedAt
User fields:   id, displayName
root args mentioning user: none - identity comes from context
Album.myReview()      -> Review
Album.isInCrate()     -> Boolean!
Query.myCrate(first,after) -> CrateConnection!
all user-scoped fields take zero args: true

Also pnpm format:check, pnpm lint, pnpm typecheck and pnpm test (3 tests) all pass.

Closes

Closes the spec half of #10. The implementation (Apollo link, context, lazy user row) is the remaining half and needs server/ from #6.

Related

@evenkkl decision 5 in docs/architecture.md is yours and I extended it rather than replacing it. Please check the transport section reads the way you intended.

@erikhfj erikhfj requested a review from a team as a code owner October 7, 2026 18:50
Copy link
Member

@robertky robertky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall. One small wording correction: deriving displayName server-side prevents the client from choosing an arbitrary display name, but it does not prevent impersonation because a client can still send another UUID in X-User-Id. That is acceptable for simulated users without authentication, but the documentation should state this precisely.

@erikhfj
Copy link
Member Author

erikhfj commented Oct 12, 2026

You are right and the wording was overclaiming. Fixed.

What deriving displayName actually buys: the client cannot choose an arbitrary name string, so a review cannot claim a display name the sender did not hold.

What it does not buy: anything about identity. X-User-Id is client-supplied, so sending someone else's UUID writes as them. That is impersonation and no amount of name derivation prevents it.

The three places that said a review "cannot be attributed to someone else" now say the opposite, in AGENTS.md rule 5, docs/architecture.md decision 5 and docs/data-model.md:

Sending someone else's UUID in X-User-Id writes as that user, so this separates visitors rather than authenticating them. Deriving displayName from the UUID stops the client choosing an arbitrary name string, but it does not stop impersonation.

Which is the honest version, and as you say, fine for simulated users. Worth having the precise version written down, because "why is this not authentication" is exactly the question that gets asked when this comes up in a review or a viva.

erikhfj and others added 2 commits October 13, 2026 12:50
Spec work for the simulated user. No runtime yet; this is the contract
the client and the server will both implement.

schema.graphql
  Adds `type User { id, displayName }` and `Review.user: User!`. Reviews
  rendered without an author were a gap: the brief asks that
  user-generated data is presented, and an unattributed review does not
  present it. Checked that no root field and no user-scoped field takes a
  userId argument, which is the point of routing identity through the
  context instead.

AGENTS.md rule 5
  Was wrong: "sent with every mutation". myReview, isInCrate and myCrate
  are queries, so they need the identity too. Rewritten to cover the
  header, the per-request context, anonymous fallback, and why displayName
  is derived server-side rather than sent by the client.

docs/architecture.md decision 5
  Even had the decision right ("sends it with each request") but not the
  transport. Adds why a header and not a GraphQL variable, cookie or
  Authorization header, the missing/invalid behaviour table, and the
  consequence that displayName is not client-supplied.

docs/data-model.md
  Adds the user to the user-generated data section and points at
  decision 5.

SDL validated with buildSchema: Review and User parse, and every
user-scoped field takes zero arguments.

Co-Authored-By: opencode <noreply@opencode.ai>
robertky pointed out the wording overclaimed. Deriving displayName
server-side stops the client choosing an arbitrary name string. It does
not stop impersonation, because X-User-Id is client-supplied and anyone
can send someone else's UUID and write as them.

That is fine for simulated users, which is what the brief allows, but the
docs should not say otherwise. Three places claimed a review "cannot be
attributed to someone else", which is the wrong guarantee.

Now stated as: the UUID separates visitors, it does not authenticate
them. Deriving the name stops the name being chosen by the client, not
the identity being claimed.

Reported by robertky.

Co-Authored-By: opencode <noreply@opencode.ai>
@erikhfj erikhfj force-pushed the feat/10-simulated-user-identity branch from ff2db5e to 56813d1 Compare October 13, 2026 18:53
@erikhfj
Copy link
Member Author

erikhfj commented Oct 13, 2026

Rebased onto main and CI is green again.

To answer the question properly: GitHub said MERGEABLE, and pull_request CI runs against the merge commit of head and base, so it was not strictly blocked. But you were right that it was worth doing. The green check on the branch was against the base as it stood a week ago, and since then main gained six commits including the whole of server/ from #42. A stale tick is not the same as a green build against what will actually merge.

There was no file overlap, so the rebase was clean. 4 files, unchanged: AGENTS.md, docs/architecture.md, docs/data-model.md, schema.graphql.

Gates against the new base, which now includes server/:

format:check  pass
lint          pass
typecheck     pass   (client + server)
test          pass   18 tests, 15 server + 3 client
build         pass

Copy link
Member

@rachelks rachelks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good now, thanks for the updates!

@rachelks rachelks merged commit d1740a3 into main Oct 13, 2026
2 checks passed
@erikhfj
Copy link
Member Author

erikhfj commented Oct 13, 2026

Please remember to delete the branch after merging, i will do it for you this time :)

@erikhfj erikhfj deleted the feat/10-simulated-user-identity branch October 13, 2026 19:19
Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants